﻿#region License

// PentaBiz - Sustainable Software Development Framework Copyright (C) 2013 Zoltán Csizmazia
// 
// This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version.
// 
// This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
// 
// You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 

#endregion

using System;
using System.AddIn.Contract;
using System.AddIn.Pipeline;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;
using System.Runtime.Serialization.Formatters;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using PentaBiz.Studio.Services.Plugins.Models;
using PentaBiz.Studio.Services.Plugins.PackageManager;

namespace PentaBiz.Studio.Services.Plugins
{
    /// <summary>
    ///     Ipc PluginLoader Proxy
    ///     <para>Loads Plugin in seperate PluginHost32|64.exe process</para>
    /// </summary>
    public class PluginHostProxy : IDisposable
    {
        private const string PluginHost64 = "PentaBiz.Studio.PluginHost64.exe";
        private const string PluginHost32 = "PentaBiz.Studio.PluginHost32.exe";
        private readonly string _pluginAssemblyName;
        private readonly string _pluginDirectory;
        private readonly PluginPackage _pluginPackage;
        private readonly List<EventWaitHandleListener> eventWaitHandles = new List<EventWaitHandleListener>();

        private string _name;
        private IPluginHost _pluginHost;


        /// <summary>
        ///     The _plugin host ready event
        /// </summary>
        private EventWaitHandle _pluginHostReadyEvent;


        private Process _process;
        private int _refCount;
        private bool disposed;

        public PluginHostProxy(PluginPackage pluginPackage)
        {
            _pluginPackage = pluginPackage;
            _pluginDirectory = string.Format(@"Plugins\{0}", pluginPackage.PackageId);
            _pluginAssemblyName = pluginPackage.AssemblyName;
        }

        public bool Is64Bit { get; private set; }

        public PluginPackage PluginPackage
        {
            get { return _pluginPackage; }
        }

        public string PluginDirectory
        {
            get { return _pluginDirectory; }
        }

        #region IDisposable Members

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        #endregion

        /// <summary>
        ///     Occurs when [plugin host terminate].
        /// </summary>
        public event EventHandler PluginHostTerminate;

        /// <summary>
        ///     Raises the <see cref="PluginHostTerminate" /> event.
        /// </summary>
        /// <param name="e">
        ///     The <see cref="EventArgs" /> instance containing the event data.
        /// </param>
        protected void OnPluginHostTerminate(EventArgs e)
        {
            EventHandler handler = PluginHostTerminate;
            if (handler != null) handler(this, e);
        }

        /// <summary>
        ///     Occurs when [plugin host error].
        /// </summary>
        public event EventHandler PluginHostError;

        /// <summary>
        ///     Raises the <see cref="PluginHostError" /> event.
        /// </summary>
        /// <param name="e">
        ///     The <see cref="EventArgs" /> instance containing the event data.
        /// </param>
        protected void OnPluginHostError(EventArgs e)
        {
            EventHandler handler = PluginHostError;
            if (handler != null) handler(this, e);
        }

        /// <summary>
        ///     Occurs when [plugin host exit].
        /// </summary>
        public event EventHandler PluginHostExit;

        /// <summary>
        ///     Raises the <see cref="PluginHostExit" /> event.
        /// </summary>
        /// <param name="e">
        ///     The <see cref="EventArgs" /> instance containing the event data.
        /// </param>
        protected void OnPluginHostExit(EventArgs e)
        {
            EventHandler handler = PluginHostExit;
            if (handler != null) handler(this, e);
        }

        public PluginTypeSelector[] GetPluginServiceTypeSelectors()
        {
            lock (this)
            {
                StartPluginHost();
                OpenPluginLoader();
            }

            return _pluginHost.GetPluginServiceTypeSelectors();
        }

        public PluginTypeSelector[] GetPluginControlTypeSelectors()
        {
            lock (this)
            {
                StartPluginHost();
                OpenPluginLoader();
            }

            return _pluginHost.GetPluginControlTypeSelectors();
        }

        public PluginTypeSelector[] GetPluginCommandTypeSelectors()
        {
            lock (this)
            {
                StartPluginHost();
                OpenPluginLoader();
            }

            return _pluginHost.GetPluginCommandTypeSelectors();
        }

        public PluginControlModel LoadPluginControl(PluginManager pluginManager, PluginTypeSelector pluginTypeSelector)
        {
            lock (this)
            {
                StartPluginHost();
                OpenPluginLoader();
            }

            INativeHandleContract contract = _pluginHost.LoadPluginControl(pluginTypeSelector);
            FrameworkElement remoteControl = FrameworkElementAdapters.ContractToViewAdapter(contract);

            var plugin = new PluginControlModel(remoteControl, this, pluginManager, pluginTypeSelector)
                {
                    Title = GetTitle(pluginTypeSelector.PluginTypeName)
                };
            Interlocked.Increment(ref _refCount);

            plugin.Disposed += OnPluginDisposed;

            return plugin;
        }

        public PluginServiceModel LoadPluginService(PluginManager pluginManager, PluginTypeSelector pluginTypeSelector)
        {
            lock (this)
            {
                StartPluginHost();
                OpenPluginLoader();
            }

            IPluginService pluginService = _pluginHost.LoadPluginService(pluginTypeSelector);

            var plugin = new PluginServiceModel(pluginService, this, pluginManager, pluginTypeSelector)
                {
                    Title = GetTitle(pluginTypeSelector.PluginTypeName)
                };
            Interlocked.Increment(ref _refCount);

            plugin.Disposed += OnPluginDisposed;

            return plugin;
        }

        public PluginCommandModel LoadPluginCommand(PluginManager pluginManager, PluginTypeSelector pluginTypeSelector)
        {
            lock (this)
            {
                StartPluginHost();
                OpenPluginLoader();
            }

            IPluginCommand pluginCommand = _pluginHost.LoadPluginCommand(pluginTypeSelector);

            var plugin = new PluginCommandModel(pluginCommand, this, pluginManager, pluginTypeSelector)
                {
                    Title = GetTitle(pluginTypeSelector.PluginTypeName)
                };
            Interlocked.Increment(ref _refCount);

            plugin.Disposed += OnPluginDisposed;

            return plugin;
        }

        private static string GetTitle(string typeName)
        {
            int dot = typeName.IndexOf('.');
            if (dot < 0 || dot >= typeName.Length - 1) return typeName;
            return typeName.Substring(dot + 1);
        }

        private void StartPluginHost()
        {
            if (_process != null) return;
            _name = "PluginHost." + Guid.NewGuid().ToString();

            string eventName = _name + ".Ready";
            _pluginHostReadyEvent = new EventWaitHandle(false, EventResetMode.ManualReset, eventName);

            //assign process name
            string processName = Is64Bit ? PluginHost64 : PluginHost32;

            var info = new ProcessStartInfo
                {
                    Arguments = _name + " " + _pluginDirectory + " " + _pluginAssemblyName,
#if !DEBUG
                CreateNoWindow = true,
#endif
                    UseShellExecute = false,
                    FileName = processName
                };

            _process = Process.Start(info);

            AddTerminateEventHandleListener();
            AddErrorEventHandleListener();
            AddExitEventHandleListener();
        }

        private void AddTerminateEventHandleListener()
        {
            var terminateEventListener = new EventWaitHandleListener(_name, "Terminate");
            terminateEventListener.EventOccured += TerminateEventListenerOnEventOccured;
            eventWaitHandles.Add(terminateEventListener);
        }

        private void TerminateEventListenerOnEventOccured(object sender, EventArgs eventArgs)
        {
            OnPluginHostTerminate(EventArgs.Empty);
        }

        private void AddErrorEventHandleListener()
        {
            var errorEventListener = new EventWaitHandleListener(_name, "Error");
            errorEventListener.EventOccured += ErrorEventListenerOnEventOccured;
            eventWaitHandles.Add(errorEventListener);
        }

        private void ErrorEventListenerOnEventOccured(object sender, EventArgs eventArgs)
        {
            OnPluginHostError(EventArgs.Empty);
        }

        private void AddExitEventHandleListener()
        {
            var exitEventListener = new EventWaitHandleListener(_name, "Exit");
            exitEventListener.EventOccured += ExitEventListenerOnEventOccured;
            eventWaitHandles.Add(exitEventListener);
        }

        private void ExitEventListenerOnEventOccured(object sender, EventArgs eventArgs)
        {
            OnPluginHostExit(EventArgs.Empty);
        }

        private void OpenPluginLoader()
        {
            if (_pluginHost != null) return;


            //wait for PluginHost ready event
            if (!_pluginHostReadyEvent.WaitOne(TimeSpan.FromSeconds(30)))
            {
                throw new InvalidOperationException("Plugin host process not ready");
            }

            //register ipc channel
            IpcChannelRegistration.RegisterChannel();

            string url = "ipc://" + _name + "/PluginLoader";
            _pluginHost = (IPluginHost) Activator.GetObject(typeof (IPluginHost), url);
        }


        /// <summary>
        ///     Called when [plugin disposed].
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="args">
        ///     The <see cref="EventArgs" /> instance containing the event data.
        /// </param>
        private void OnPluginDisposed(object sender, EventArgs args)
        {
            Interlocked.Decrement(ref _refCount);

            var pluginModel = sender as IPluginModel;

            if (pluginModel != null)
                pluginModel.Disposed -= OnPluginDisposed;


            if (_refCount == 0)
            {
                Terminate();
            }
        }

        internal void Terminate()
        {
            lock (this)
            {
                if (_pluginHost != null)
                {
                    _pluginHost.Terminate();
                    _pluginHost = null;
                }
                _process = null;

                foreach (EventWaitHandleListener eventWaitHandleListener in eventWaitHandles)
                {
                    eventWaitHandleListener.Dispose();
                }
                eventWaitHandles.Clear();

                //dispose waitevents
                if (_pluginHostReadyEvent != null)
                {
                    _pluginHostReadyEvent.Dispose();
                    _pluginHostReadyEvent = null;
                }
            }
        }

        private void Dispose(bool disposing)
        {
            if (disposed)
                return;

            if (disposing)
            {
                Terminate();
            }

            disposed = true;
        }

        ~PluginHostProxy()
        {
            Dispose(false);
        }

        #region Nested type: EventWaitHandleListener

        private class EventWaitHandleListener : IDisposable
        {
            private readonly CancellationTokenSource _cancellationTokenSource;

            private readonly EventWaitHandle _eventWaitHandle;

            private readonly Task _task;

            private bool disposed;

            public EventWaitHandleListener(string name, string @event)
            {
                HandleName = string.Format("{0}.{1}", name, @event);

                _eventWaitHandle = new EventWaitHandle(false, EventResetMode.ManualReset, HandleName);


                _cancellationTokenSource = new CancellationTokenSource();
                CancellationToken ct = _cancellationTokenSource.Token;

                _task = Task.Run(() =>
                    {
                        _eventWaitHandle.WaitOne();
                        OnEventOccured(EventArgs.Empty);
                    }, ct);
            }


            public string HandleName { get; set; }

            #region IDisposable Members

            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }

            #endregion

            public event EventHandler EventOccured;

            public void OnEventOccured(EventArgs e)
            {
                EventHandler handler = EventOccured;
                if (handler != null) handler(this, e);
            }

            private void Dispose(bool disposing)
            {
                if (disposed)
                    return;

                if (disposing)
                {
                    if (!_task.IsCompleted)
                    {
                        _cancellationTokenSource.Cancel();

                        //_task.Dispose();

                        _cancellationTokenSource.Dispose();
                        _eventWaitHandle.Dispose();
                    }
                }

                disposed = true;
            }

            ~EventWaitHandleListener()
            {
                Dispose(false);
            }
        }

        #endregion

        #region Nested type: IpcChannelRegistration

        private class IpcChannelRegistration
        {
            private static readonly object _lock = new object();
            private static bool _registered;

            public static void RegisterChannel()
            {
                lock (_lock)
                {
                    if (_registered) return;

                    var serverProvider = new BinaryServerFormatterSinkProvider {TypeFilterLevel = TypeFilterLevel.Full};
                    var clientProvider = new BinaryClientFormatterSinkProvider();
                    var properties = new Hashtable();
                    properties["portName"] = Guid.NewGuid().ToString();
                    var channel = new IpcChannel(properties, clientProvider, serverProvider);

                    ChannelServices.RegisterChannel(channel, false);
                    _registered = true;
                }
            }
        }

        #endregion
    }
}